/*
 * Created on Jul 11, 2004
 */
package dk.aiszone.lang.autosource.generatorobjects;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import dk.aiszone.lang.autosource.generatorobjects.interfaces.GeneratorObject;

/**
 * The <code>Class</code> contains class information used by the <code>SourceGenerator</code>.<br>
 * Class information covers the actual class name and the package it resides in.<br>
 * The <code>Class</code> object does not support generics declaration, but has an extending class,<br>
 * which adds supports for generics declaration - see {@link GenericClass}.
 * 
 * @author <a href="mailto:ais@mail.dk">Ais</a>
 * @since 1.0
 * @version 1.0
 */
public class Class extends GeneratorObject implements Cloneable, Comparable<Class> {

    /**
     * Special instance of the <code>Class</code> object.<br>
     * This static instance represents the constructor of a body.
     */
    public static final Class CONSTRUCTOR = new Class(false);

    /**
     * Special instance of the <code>Class</code> object.<br>
     * This static instance represents the void return argument of a <code>Method</code> or <code>MethodDeclaration</code>
     */
    public static final Class VOID = new Class(true);

    private String _class;
    private String _package;

    private int arrayDimension;

    private List<GenericClass> generics;
    private boolean isConstructor;
    private boolean isVoid;

    /**
     * Default constructor for the <code>Class</code> object.
     */
    public Class() {
        generics = new ArrayList<GenericClass>();
    }

    /**
     * Constructor for the <code>Class</code> object.
     * 
     * @param _package
     *            The package to represent.<br>
     *            The supplied package is expected to the the class name including the complete package path.<br>
     * @see #setPackage(String)
     */
    public Class(String _package) {
        this();
        setPackage(_package);
    }

    /**
     * Constructor for the <code>Class</code> object.
     * 
     * @param _package
     *            The package to represent.<br>
     *            The supplied package is expected to the the class name including the complete package path.
     * @param generics
     *            <code>Array</code> of <code>GenericClass</code> objects.
     * @see #setPackage(String)
     */
    public Class(String _package, GenericClass... generics) {
        this(_package);
        this.generics = Arrays.asList(generics);
    }

    /**
     * Constructor for the <code>Class</code> object.
     * 
     * @param _package
     *            The package to represent.<br>
     *            The supplied package is expected to the the class name including the complete package path.
     * @param arrayDimension
     *            <code>int</code> specifying the number of dimensions of the <code>Array</code>.
     */
    public Class(String _package, int arrayDimension) {
        this(_package);
        this.arrayDimension = arrayDimension;
    }

    /**
     * Constructor for the <code>Class</code> object.
     * 
     * @param packageName
     *            The package to represent.
     * @param className
     *            The name of the class.
     */
    public Class(String packageName, String className) {
        this();
        if (packageName != null) {
            this._package = packageName + "." + className;
        } else {
            this._package = className;
        }
        this._class = className;
    }

    /**
     * Constructor for the <code>Class</code> object.
     * 
     * @param packageName
     *            The package to represent.
     * @param className
     *            The name of the class.
     * @param generics
     *            <code>Array</code> of <code>GenericClass</code> objects.
     */
    public Class(String packageName, String className, GenericClass... generics) {
        this(packageName, className);
        this.generics = Arrays.asList(generics);
    }

    private Class(boolean isVoid) {
        this();
        this.isVoid = isVoid;
        this.isConstructor = !isVoid;
    }

    /**
     * Add a generic to the class.<br>
     * Ex:
     * 
     * <pre>
     *  Set&lt;Object&gt;
     * </pre>
     * 
     * @param generic
     *            The <code>GenericClass</code> to add.
     * @return <code>this</code>.
     */
    public Class addGeneric(GenericClass generic) {
        generics.add(generic);
        return this;
    }

    @Override
    public Object clone() {
        if (this == CONSTRUCTOR || this == VOID) {
            return this;
        }

        Class clone = new Class();
        clone._class = _class;
        clone._package = _package;

        for (Iterator<GenericClass> iter = generics.iterator(); iter.hasNext();) {
            clone.generics.add((GenericClass)iter.next().clone());
        }

        clone.arrayDimension = arrayDimension;
        clone.isConstructor = isConstructor;
        clone.isVoid = isVoid;

        return clone;
    }

    @Override
    public int compareTo(Class o) {
        return getPackageName().compareTo(o.getPackageName());
    }

    /**
     * Retrieve the number of dimensions of the array.
     * 
     * @return The number of dimensions.
     */
    public int getArrayDimensions() {
        return arrayDimension;
    }

    /**
     * Retrive the name of the <code>Class</code>
     * 
     * @return The class name
     */
    public String getClassName() {
        return _class;
    }

    /**
     * Retrieve the <code>List</code>
     * 
     * @return Unmodifiable <code>List</code> of generics.
     */
    public List<GenericClass> getGenerics() {
        return Collections.unmodifiableList(generics);
    }

    /**
     * Retrives the complete packagename including the class name
     * 
     * @return The complete package name
     */
    public String getPackageName() {
        return _package;
    }

    /**
     * Retrieve the package path of the <code>Class</code>.<br>
     * Ex. <i>java.util.Map</i> will return <i>java.util</i>.
     * 
     * @return The package path.
     */
    public String getPackagePath() {
        if (_package.length() == _class.length()) {
            return null;
        }

        return _package.substring(0, _package.length() - _class.length() - 1);
    }

    /**
     * Retrieve whether the <code>Class</code> declares an <code>Array</code>.
     * 
     * @return <code>true</code> if an array - otherwise <code>false</code>.
     */
    public boolean isArray() {
        return arrayDimension > 0;
    }

    /**
     * Retrieve whether the <code>Class</code> declares a <code>constructor</code>.
     * 
     * @return <code>true</code> if a constructor - otherwise <code>false</code>.
     */
    public boolean isConstructor() {
        return isConstructor;
    }

    /**
     * Retrieve whether the <code>Class</code> defines a primitive class.<br>
     * The following classes are interpreted as primitives:
     * <ul>
     * <li>long
     * <li>int
     * <li>char
     * <li>boolean
     * <li>double
     * <li>float
     * <li>short
     * <li>byte
     * </ul>
     * 
     * @return <code>true</code> if primitive - otherwise <code>false</code>.
     */
    public boolean isPrimitive() {
        if ("long".equals(_class)) {
            return true;
        }
        if ("int".equals(_class)) {
            return true;
        }
        if ("char".equals(_class)) {
            return true;
        }
        if ("boolean".equals(_class)) {
            return true;
        }
        if ("double".equals(_class)) {
            return true;
        }
        if ("float".equals(_class)) {
            return true;
        }
        if ("byte".equals(_class)) {
            return true;
        }
        if ("short".equals(_class)) {
            return true;
        }

        return false;
    }

    /**
     * Retrieve whether the <code>Class</code> declares <code>void</code>.
     * 
     * @return <code>true</code> if void - otherwise <code>false</code>.
     */
    public boolean isVoid() {
        return isVoid;
    }

    /**
     * Retrives information about whether this <code>Class</code> requires to imported
     * 
     * @return true if import is required - otherwise false
     */
    public boolean requiresImport() {
        boolean requires = doesThisRequireImport();

        if (!requires) {
            for (Iterator<GenericClass> iter = generics.iterator(); iter.hasNext();) {
                requires |= iter.next().requiresImport();
            }
        }

        return requires;
    }

    /**
     * Set or update the package of the <code>Class</code>.<br>
     * <b>Warning:</b> Calling this method will reset the number of dimensions.
     * 
     * @param _package
     *            The package to represent.<br>
     *            The supplied package is expected to the the class name including the complete package path.<br>
     *            The format of <code>java.lang.Class#getName()</code> is also supported.
     */
    public void setPackage(String _package) {
        arrayDimension = 0;
        while (_package.charAt(arrayDimension) == '[') {
            arrayDimension++;
        }
        _package = _package.substring(arrayDimension);

        int index = _package.lastIndexOf('.');
        if (index > 0) {
            this._package = _package;
            this._class = _package.substring(index + 1);
        } else if (_package.length() == 1) {
            if (_package.equals("Z")) {
                this._class = "boolean";
            } else if (_package.equals("B")) {
                this._class = "byte";
            } else if (_package.equals("C")) {
                this._class = "char";
            } else if (_package.equals("D")) {
                this._class = "double";
            } else if (_package.equals("F")) {
                this._class = "float";
            } else if (_package.equals("I")) {
                this._class = "int";
            } else if (_package.equals("J")) {
                this._class = "long";
            } else if (_package.equals("S")) {
                this._class = "short";
            } else {
                this._class = _package;
            }

            this._package = this._class;
        } else {
            this._package = _package;
            this._class = _package;
        }
    }

    /**
     * Converts the <code>Class</code> and any generics to a <code>Set</code> of <code>Imports</code>.
     * 
     * @return The needed <code>Imports</code> to support this <code>Class</code> (<code>Set&lt;Import&gt;</code>).
     */
    public Set<Import> toImports() {
        Set<Import> res = new HashSet<Import>();

        if (doesThisRequireImport()) {
            res.add(new Import(_package));
        }

        for (Iterator<GenericClass> iter = generics.iterator(); iter.hasNext();) {
            res.addAll(iter.next().toImports());
        }

        return res;
    }

    private boolean doesThisRequireImport() {
        if (isVoid || isConstructor) {
            return false;
        }

        if (_package.startsWith("java.lang", 0)) {
            if (_package.indexOf(".", "java.lang.".length()) > 0) {
                return true;
            }

            return false;
        }

        if (_package == _class) {
            return false;
        }

        return true;
    }
}